﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace FixedWidth.Configuration.Rules
{
	public class RowRule
	{
		private static readonly Common.Logging.ILog Logger = Common.Logging.LogManager.GetCurrentClassLogger();

		/// <summary>
		/// Gets or sets the number of rows to skip at the beginning of the document.
		/// </summary>
		public int ExcludeFirst
		{
			get { return _ExcludeFirst; }
			set
			{
				if (value < 0) throw new ArgumentOutOfRangeException("The ExcludeFirst property needs to be a non-negative number.");
				_ExcludeFirst = value;
			}
		}
		private int _ExcludeFirst = 0;

		/// <summary>
		/// Gets or sets the number of rows to skip from the end of the document.
		/// </summary>
		public int ExcludeLast
		{
			get { return _ExcludeLast; }
			set
			{
				if (value < 0) throw new ArgumentOutOfRangeException("The ExcludeLast property needs to be a non-negative number.");
				_ExcludeLast = value;
			}
		}
		private int _ExcludeLast = 0;

		/// <summary>
		/// Gets or sets whether or not a parsing error throws an exception.
		/// </summary>
		public bool SuppressParseErrors { get; set; }

		/// <summary>
		/// Gets or sets whether the line end of the file is uniform.
		/// If this is true, suppresses errors where a column rule
		/// attempts to read a text postion beyond the end of the line.
		/// </summary>
		public bool JaggedEdge { get; set; }

		/// <summary>
		/// Gets or sets the row evaluator for determining if the row is 
		/// desired to be parsed.  Default is AllRowEvaluator.
		/// </summary>
		public IRowEvaluator Evaluator
		{
			get { return _Evaluator; }
			set { _Evaluator = value; }
		}
		private IRowEvaluator _Evaluator = new AllRowEvaluator();

		/// <summary>
		/// Gets the column parsing rules for the rows.
		/// </summary>
		public IList<IColumnRule> ColumnRules
		{
			get
			{
				return _ColumnRules;
			}
		}
		private List<IColumnRule> _ColumnRules = new List<IColumnRule>();

		/// <summary>
		/// Gets or sets the character string that separates rows.  Default
		/// is a carriage-return/ line-feed.
		/// </summary>
		public string RowDelimiter
		{
			get { return _RowDelimiter; }
			set
			{
				if (string.IsNullOrEmpty(value)) throw new ArgumentException("The RowDelimiter property cannot be null or empty.");
				_RowDelimiter = value;
			}
		}
		private string _RowDelimiter = "\r\n";

		/// <summary>
		/// A Fluent configuration interface for the RowRule object.
		/// </summary>
		public RowRuleFluentConfiguration Configure { get; protected set; }

		public RowRule()
		{
			Configure = new RowRuleFluentConfiguration(this);
		}

		/// <summary>
		/// Parse the fixed-width document from a stream.
		/// </summary>
		/// <param name="document"></param>
		/// <returns></returns>
		public IList<IDictionary<string, ParsedColumn>> Parse(Stream document)
		{
			string data = string.Empty;
			using (var sr = new StreamReader(document))
			{
				data = sr.ReadToEnd();
			}
			return Parse(data);
		}

		/// <summary>
		/// Parse the fixed width document from a string.
		/// </summary>
		/// <param name="document"></param>
		/// <returns></returns>
		public IList<IDictionary<string, ParsedColumn>> Parse(string document)
		{
			//verify we have column rules
			if (ColumnRules.Count == 0) throw new InvalidOperationException("No column rules defined!");

			//verify we don't have duplicate column names
			if (ColumnRules.Count() != ColumnRules.Select(x => x.Name).Distinct().Count())
				throw new ArgumentException("Duplicate column names detected.");

			var rows = document.Split(this.RowDelimiter.ToCharArray());
			var values = new List<IDictionary<string, ParsedColumn>>();

			//loop through the rows
			for (int i = ExcludeFirst; i < rows.Length - ExcludeLast; i++)
			{
				string row = rows[i];

				if (Evaluator.IncludeRow(row))
				{
					//the row is to be parsed.  Parse it... NOW!
					var parsed = new Dictionary<string, ParsedColumn>();
					foreach (var rule in ColumnRules)
					{
						try
						{
							var col = new ParsedColumn()
							{
								Name = rule.Name,
								Type = rule.Type,
								Value = rule.Parse(row)
							};
							parsed[rule.Name] = col;
						}
						catch (ParseException ex)
						{
							if (!SuppressParseErrors) throw;
							Logger.Debug(d => d("Failed to parse column '{0}' on line {1}: {2}", rule.Name, i + 1, ex.Message));
						}
						catch (EndOfRowException)
						{
							if (!JaggedEdge) throw;
							parsed[rule.Name] = new ParsedColumn()
							{
								Name = rule.Name,
								Type = rule.Type,
								Value = GetDefault(rule.Type)
							};
						}
					}
					values.Add(parsed);
				}
			}
			return values;
		}

		private object GetDefault(Type type)
		{
			if (type.IsValueType)
			{
				return Activator.CreateInstance(type);
			}
			return null;
		}

		public class RowRuleFluentConfiguration
		{
			private readonly RowRule _rule;

			public RowRuleFluentConfiguration(RowRule rule)
			{
				_rule = rule;
			}

			/// <summary>
			/// Set how many lines to skip at the beginning of the document.
			/// </summary>
			/// <param name="lines"></param>
			/// <returns></returns>
			public RowRuleFluentConfiguration ExcludeFirst(int lines)
			{
				_rule.ExcludeFirst = lines;
				return this;
			}

			/// <summary>
			/// Set how many lines to skip at the end of the document.
			/// </summary>
			/// <param name="lines"></param>
			/// <returns></returns>
			public RowRuleFluentConfiguration ExcludeLast(int lines)
			{
				_rule.ExcludeLast = lines;
				return this;
			}

			/// <summary>
			/// Add a column rule for parsing the data.
			/// </summary>
			/// <param name="rule"></param>
			/// <returns></returns>
			public RowRuleFluentConfiguration AddColumnRule(ColumnRule rule)
			{
				_rule.ColumnRules.Add(rule);
				return this;
			}

			/// <summary>
			/// Set the row evaluator for this row.
			/// </summary>
			/// <param name="evaluator"></param>
			/// <returns></returns>
			public RowRuleFluentConfiguration SetRowEvaluator(IRowEvaluator evaluator)
			{
				_rule.Evaluator = evaluator;
				return this;
			}

			/// <summary>
			/// Do not throw an exception if a row cannot be parsed; just continue
			/// on parsing the rest.
			/// </summary>
			/// <returns></returns>
			public RowRuleFluentConfiguration SuppressParseErrors()
			{
				_rule.SuppressParseErrors = true;
				return this;
			}

			/// <summary>
			/// Set the row delimiter for the document.  The default is carriage-
			/// return/ line-feed (chr 13, chr 10), or "\r\n"
			/// </summary>
			/// <param name="delimiter"></param>
			/// <returns></returns>
			public RowRuleFluentConfiguration RowDelimiter(string delimiter)
			{
				_rule.RowDelimiter = delimiter;
				return this;
			}

			/// <summary>
			/// Do not throw an exception if a column rule tries to read beyond 
			/// the end of a row.
			/// </summary>
			/// <returns></returns>
			public RowRuleFluentConfiguration JaggedEdge()
			{
				_rule.JaggedEdge = true;
				return this;
			}

			/// <summary>
			/// Explicitly return the configured RowRule object.
			/// </summary>
			/// <returns></returns>
			public RowRule Build()
			{
				return _rule;
			}

			public static implicit operator RowRule(RowRuleFluentConfiguration config)
			{
				return config._rule;
			}
		}
	}
}
